@data-fair/dev-server 1.13.1 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/package.json +47 -62
  2. package/src/app.js +273 -0
  3. package/src/app.ts +278 -0
  4. package/src/config.js +7 -0
  5. package/src/config.ts +12 -0
  6. package/src/index.js +21 -0
  7. package/{server/index.js → src/index.ts} +6 -5
  8. package/src/ui-config.js +3 -0
  9. package/src/ui-config.ts +6 -0
  10. package/ui/dist/assets/index-B_eKgwUO.css +5 -0
  11. package/ui/dist/assets/index-C7IbVpbV.js +167 -0
  12. package/ui/dist/fonts/XRXV3I6Li01BKofIMeaBXso-C3IBG1kp.woff2 +0 -0
  13. package/ui/dist/fonts/XRXV3I6Li01BKofINeaB-BaTF6Vo7.woff2 +0 -0
  14. package/ui/dist/fonts/XRXV3I6Li01BKofIO-aBXso-DcJfvmGA.woff2 +0 -0
  15. package/ui/dist/fonts/XRXV3I6Li01BKofIOOaBXso-BWI5zH9R.woff2 +0 -0
  16. package/ui/dist/fonts/XRXV3I6Li01BKofIOuaBXso-B55YuedR.woff2 +0 -0
  17. package/ui/dist/fonts/XRXX3I6Li01BKofIMNaDRs4-BbMn9XSX.woff2 +0 -0
  18. package/ui/dist/fonts/XRXX3I6Li01BKofIMNaHRs71cA-Cznx39fA.woff2 +0 -0
  19. package/ui/dist/fonts/XRXX3I6Li01BKofIMNaMRs71cA-CuWrHpFO.woff2 +0 -0
  20. package/ui/dist/fonts/XRXX3I6Li01BKofIMNaNRs71cA-D1eeM49Z.woff2 +0 -0
  21. package/ui/dist/fonts/XRXX3I6Li01BKofIMNaORs71cA-Bm_i0Dk1.woff2 +0 -0
  22. package/ui/dist/index.html +19 -0
  23. package/.eslintignore +0 -5
  24. package/.nvmrc +0 -1
  25. package/config/custom-environment-variables.js +0 -23
  26. package/config/default.js +0 -18
  27. package/config/development.js +0 -1
  28. package/config/production.js +0 -1
  29. package/dist/.nojekyll +0 -0
  30. package/dist/200.html +0 -9
  31. package/dist/_dev-server-nuxt/2c7d7c0.js +0 -1
  32. package/dist/_dev-server-nuxt/3091234.js +0 -1
  33. package/dist/_dev-server-nuxt/3f92c7d.js +0 -1
  34. package/dist/_dev-server-nuxt/439df67.js +0 -1
  35. package/dist/_dev-server-nuxt/5a68472.js +0 -1
  36. package/dist/_dev-server-nuxt/LICENSES +0 -52
  37. package/dist/_dev-server-nuxt/a62e828.js +0 -1
  38. package/dist/_dev-server-nuxt/b39b908.js +0 -1
  39. package/dist/_dev-server-nuxt/b64ddf5.js +0 -2
  40. package/dist/_dev-server-nuxt/c306f6e.js +0 -1
  41. package/dist/_dev-server-nuxt/c5e103d.js +0 -1
  42. package/dist/_dev-server-nuxt/dc90835.js +0 -2
  43. package/dist/_dev-server-nuxt/eb298ad.js +0 -8
  44. package/dist/index.html +0 -9
  45. package/nodemon.json +0 -5
  46. package/nuxt.config.js +0 -59
  47. package/server/app.js +0 -308
package/package.json CHANGED
@@ -1,19 +1,26 @@
1
1
  {
2
2
  "name": "@data-fair/dev-server",
3
- "version": "1.13.1",
3
+ "version": "2.0.1",
4
4
  "description": "A development server for optimal development experience of data-fair applications.",
5
- "main": "server/index.js",
5
+ "main": "src/index.js",
6
6
  "bin": {
7
- "df-dev-server": "server/index.js"
7
+ "df-dev-server": "src/index.js"
8
8
  },
9
+ "type": "module",
9
10
  "scripts": {
10
- "dev": "NODE_ENV=development nodemon server",
11
- "dev-src": "http-server test-apps/minimal --port=3000 --cors --silent",
12
- "dev-vue3": "APP_URL=http://localhost:3000/app/ APP_SCRIPT=dev-src-vue3 NODE_ENV=development nodemon server",
13
- "dev-src-vue3": "cd test-apps/vue3 && npm run dev",
14
- "prepare": "npm run lint && nuxt generate",
15
- "lint": "eslint --ext .js,.vue ."
11
+ "build": "tsc",
12
+ "dev": "NODE_ENV=development node --watch src/index.js",
13
+ "dev-test-app-minimal": "http-server test-apps/minimal --port=3000 --cors --silent",
14
+ "dev-test-app-vue3": "cd test-apps/vue3 && npm run dev",
15
+ "dev-zellij": "zellij --layout .zellij.kdl",
16
+ "prepublishOnly": "npm run lint && npm run build &&npm -w ui run build",
17
+ "lint": "eslint . && npm -w ui run lint",
18
+ "lint-fix": "eslint --fix . && npm -w ui run lint-fix",
19
+ "build-types": "df-build-types ."
16
20
  },
21
+ "workspaces": [
22
+ "ui"
23
+ ],
17
24
  "repository": {
18
25
  "type": "git",
19
26
  "url": "git+https://github.com/koumoul-dev/df-dev-server.git"
@@ -22,66 +29,44 @@
22
29
  "data-fair",
23
30
  "dev"
24
31
  ],
25
- "author": "Alban Mouton <alban.mouton@gmail.com>",
32
+ "author": "Alban Mouton <alban.mouton@koumoul.com>",
26
33
  "license": "MIT",
27
34
  "bugs": {
28
35
  "url": "https://github.com/koumoul-dev/df-dev-server/issues"
29
36
  },
30
37
  "homepage": "https://github.com/koumoul-dev/df-dev-server#readme",
31
- "eslintConfig": {
32
- "root": true,
33
- "env": {
34
- "node": true
35
- },
36
- "extends": [
37
- "plugin:vue/essential",
38
- "eslint:recommended",
39
- "standard",
40
- "plugin:vue/recommended"
41
- ],
42
- "rules": {
43
- "vue/no-v-html": "off",
44
- "vue/multi-word-component-names": "off"
45
- }
38
+ "devDependencies": {
39
+ "@data-fair/lib-types-builder": "^1.8.2",
40
+ "@types/config": "^3.3.5",
41
+ "@types/cors": "^2.8.19",
42
+ "@types/debug": "^4.1.12",
43
+ "@types/express": "^5.0.3",
44
+ "@types/node": "^24.0.7",
45
+ "@types/ws": "^8.18.1",
46
+ "eslint": "^9.30.0",
47
+ "eslint-plugin-vue": "^10.2.0",
48
+ "eslint-plugin-vuetify": "^2.5.2",
49
+ "http-server": "^14.1.1",
50
+ "neostandard": "^0.12.1",
51
+ "typescript": "^5.8.3"
46
52
  },
47
53
  "dependencies": {
48
- "@koumoul/nuxt-config-inject": "^0.4.4",
49
- "@koumoul/vjsf": "^2.18.0",
50
- "@koumoul/v-iframe": "^1.3.4",
51
- "ajv": "^8.8.2",
52
- "ajv-i18n": "^4.2.0",
53
- "body-parser": "^1.19.1",
54
- "chalk": "^3.0.0",
55
- "config": "^3.3.6",
54
+ "@data-fair/lib-express": "^1.20.4",
55
+ "@parse5/tools": "^0.6.0",
56
+ "chalk": "^5.4.1",
57
+ "config": "^4.0.0",
56
58
  "cors": "^2.8.5",
57
- "dot-prop": "^6.0.1",
58
- "dotenv": "^10.0.0",
59
- "escape-string-regexp": "^2.0.0",
60
- "event-to-promise": "^0.8.0",
61
- "express": "^4.17.2",
62
- "fs-extra": "^10.0.0",
63
- "html-extractor": "^0.2.2",
64
- "http-proxy-middleware": "^2.0.1",
65
- "iframe-resizer": "^4.3.2",
66
- "parse5": "^6.0.1",
67
- "tree-kill": "^1.2.2",
68
- "ws": "^8.3.0"
59
+ "debug": "^4.4.1",
60
+ "dotenv": "^17.0.0",
61
+ "express": "^5.1.0",
62
+ "http-proxy-middleware": "^3.0.5",
63
+ "parse5": "^7.3.0",
64
+ "prom-client": "^15.1.3",
65
+ "ws": "^8.18.3"
69
66
  },
70
- "devDependencies": {
71
- "@babel/eslint-parser": "^7.16.0",
72
- "@nuxtjs/axios": "^5.13.6",
73
- "@nuxtjs/i18n": "^7.2.0",
74
- "@nuxtjs/vuetify": "^1.12.3",
75
- "eslint": "^7.12.1",
76
- "eslint-config-standard": "^16.0.3",
77
- "eslint-plugin-import": "^2.25.2",
78
- "eslint-plugin-node": "^11.1.0",
79
- "eslint-plugin-promise": "^5.1.1",
80
- "eslint-plugin-vue": "^8.0.3",
81
- "http-server": "^0.12.3",
82
- "nodemon": "^2.0.15",
83
- "nuxt": "^2.15.8",
84
- "reconnecting-websocket": "^4.4.0",
85
- "vuetify": "^2.6.1"
86
- }
67
+ "files": [
68
+ "./config/dist/**/*",
69
+ "./ui/dist/**/*",
70
+ "./src/**/*"
71
+ ]
87
72
  }
package/src/app.js ADDED
@@ -0,0 +1,273 @@
1
+ // Express app for TaxMan own API and UI
2
+ import config from './config.js';
3
+ import uiConfig from './ui-config.js';
4
+ import { WebSocket, WebSocketServer } from 'ws';
5
+ import { createServer } from 'node:http';
6
+ import express from 'express';
7
+ import cors from 'cors';
8
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
9
+ import debugModule from 'debug';
10
+ import { createProxyMiddleware, fixRequestBody } from 'http-proxy-middleware';
11
+ import * as parse5 from 'parse5';
12
+ import chalk from 'chalk';
13
+ import { isElementNode, createTextNode, createElement } from '@parse5/tools';
14
+ import escapeStringRegexp from 'escape-string-regexp';
15
+ import eventPromise from '@data-fair/lib-utils/event-promise.js';
16
+ import { createSpaMiddleware } from '@data-fair/lib-express/serve-spa.js';
17
+ import { resolve } from 'node:path';
18
+ const debug = debugModule('df-dev-server');
19
+ const app = express();
20
+ const server = createServer(app);
21
+ // web socket server, used both by dev-server UI (opened on / path)
22
+ // and by application (opened on /data-fair), in this case we transmit all messages to and from data-fair
23
+ const wss = new WebSocketServer({ noServer: true });
24
+ let devServerWS;
25
+ let dataFairWS;
26
+ server.on('upgrade', function upgrade(req, socket, head) {
27
+ wss.handleUpgrade(req, socket, head, (ws) => {
28
+ if (req.url === '/data-fair') {
29
+ if (dataFairWS)
30
+ dataFairWS.terminate();
31
+ dataFairWS = ws;
32
+ ws.on('message', (data) => {
33
+ if (dataFairOutputWS) {
34
+ const message = JSON.parse(data);
35
+ if (message.type === 'subscribe')
36
+ message.apiKey = config.dataFair.apiKey;
37
+ dataFairOutputWS.send(JSON.stringify(message));
38
+ }
39
+ });
40
+ }
41
+ else {
42
+ if (devServerWS)
43
+ devServerWS.terminate();
44
+ devServerWS = ws;
45
+ }
46
+ wss.emit('connection', ws, req);
47
+ });
48
+ });
49
+ const dfWsUrl = config.dataFair.url.replace('http://', 'ws://').replace('https://', 'wss://') + '/';
50
+ const dataFairOutputWS = new WebSocket(dfWsUrl);
51
+ dataFairOutputWS.on('open', () => dataFairOutputWS.send('connect to external websocket server ' + dfWsUrl));
52
+ dataFairOutputWS.on('message', (data) => { if (dataFairWS)
53
+ dataFairWS.send(data.toString()); });
54
+ dataFairOutputWS.on('error', (err) => console.error('failed to connect to external websocket server', dfWsUrl, err));
55
+ app.use(cors());
56
+ app.use(express.json());
57
+ // very basic CRUD of config
58
+ app.get('/config', (req, res, next) => {
59
+ const devConfig = existsSync('.dev-config.json') ? JSON.parse(readFileSync('.dev-config.json', 'utf8')) : {};
60
+ debug('read dev config', devConfig);
61
+ res.send(devConfig);
62
+ });
63
+ app.put('/config', (req, res, next) => {
64
+ debug('save dev config', req.body);
65
+ writeFileSync('.dev-config.json', JSON.stringify(req.body, null, 2));
66
+ res.send(req.body);
67
+ });
68
+ app.post('/config/error', (req, res) => {
69
+ console.log('Application sent an error', req.body);
70
+ if (devServerWS)
71
+ devServerWS.send(JSON.stringify({ type: 'app-error', data: req.body }));
72
+ else
73
+ console.error('no dev-server websocket open to send error to');
74
+ res.send();
75
+ });
76
+ // re-expose the application performing similar modifications to the body as data-fair
77
+ const appUrl = new URL(config.app.url);
78
+ const appPrefix = appUrl.pathname === '/' ? '' : appUrl.pathname;
79
+ app.use('/app', createProxyMiddleware({
80
+ target: appUrl.origin,
81
+ secure: false,
82
+ changeOrigin: true,
83
+ selfHandleResponse: true, // so that the onProxyRes takes care of sending the response
84
+ on: {
85
+ proxyReq(proxyReq, req, res) {
86
+ proxyReq.path = appPrefix + proxyReq.path;
87
+ proxyReq.setHeader('Accept-Encoding', 'identity'); // disable compression
88
+ },
89
+ proxyRes(proxyRes, req, res) {
90
+ const configuration = existsSync('.dev-config.json') ? JSON.parse(readFileSync('.dev-config.json', 'utf8')) : {};
91
+ // console.log('inject config', configuration)
92
+ const dataBuffers = [];
93
+ proxyRes.on('data', (data) => { dataBuffers.push(data); });
94
+ proxyRes.on('end', async () => {
95
+ try {
96
+ let output = Buffer.concat(dataBuffers).toString();
97
+ if (output.includes('%APPLICATION%')) {
98
+ const filledBody = output.replace(/%APPLICATION%/g, JSON.stringify({
99
+ id: 'dev-application',
100
+ title: 'Dev application',
101
+ configuration,
102
+ exposedUrl: `http://localhost:${config.port}/app`,
103
+ href: `http://localhost:${config.port}/config`,
104
+ apiUrl: `http://localhost:${config.port}/data-fair/api/v1`,
105
+ wsUrl: `ws://localhost:${config.port}/data-fair`,
106
+ owner: config.dataFair && config.dataFair.owner
107
+ }));
108
+ const document = parse5.parse(filledBody);
109
+ const html = document.childNodes.filter(isElementNode).find(c => c.tagName === 'html');
110
+ if (!html)
111
+ throw new Error('HTML structure is broken, expect html, head and body elements');
112
+ const headNode = html.childNodes.filter(isElementNode).find(c => c.tagName === 'head');
113
+ const bodyNode = html.childNodes.filter(isElementNode).find(c => c.tagName === 'body');
114
+ if (!headNode || !bodyNode)
115
+ throw new Error('HTML structure is broken, expect html, head and body elements');
116
+ // add a script to simulate instrumentation by capture service
117
+ // @ts-ignore
118
+ const query = req.query;
119
+ if (query.thumbnail === 'true') {
120
+ const script = createElement('script', { type: 'text/javascript' });
121
+ script.childNodes.push(createTextNode(`
122
+ console.log('[capture] Simulate a screenshot capture context')
123
+ var triggerCalled = false
124
+ window.triggerCapture = function (animationSupported) {
125
+ triggerCalled = true
126
+ console.log('[capture] triggerCapture called')
127
+ if (animationSupported) {
128
+ console.log('[capture] this application supports animated screenshots')
129
+ var i = 0
130
+ const interval = setInterval(function () {
131
+ i++
132
+ if (i === 1800) {
133
+ console.error('[capture] stop after the maximum number of frames was attained')
134
+ clearInterval(interval)
135
+ }
136
+ var stopped = window.animateCaptureFrame()
137
+ if (stopped) {
138
+ console.log('[capture] animation was stopped after ' + i + ' frames')
139
+ clearInterval(interval)
140
+ }
141
+ }, 67)
142
+ return true
143
+ } else {
144
+ console.log('[capture] this application does not support animated screenshots')
145
+ }
146
+ }
147
+ setTimeout(function() {
148
+ if (!triggerCalled) {
149
+ console.error('[capture] triggerCapture was not called after a 5s wait')
150
+ }
151
+ }, 5000)
152
+ `));
153
+ headNode.childNodes.push(script);
154
+ }
155
+ const meta = {};
156
+ for (const node of headNode.childNodes.filter(isElementNode)) {
157
+ if (node.tagName === 'meta') {
158
+ const name = node.attrs.find(a => a.name === 'name')?.value;
159
+ const content = node.attrs.find(a => a.name === 'content')?.value;
160
+ if (name !== undefined && content !== undefined)
161
+ meta[name] = content;
162
+ }
163
+ }
164
+ if (meta['df:sync-state'] === 'true' || meta['df:overflow'] === 'true') {
165
+ bodyNode.childNodes.push(createElement('script', {
166
+ type: 'text/javascript',
167
+ src: 'https://cdn.jsdelivr.net/npm/@data-fair/frame@0.12/dist/v-iframe-compat/d-frame-content.min.js'
168
+ }));
169
+ }
170
+ output = parse5.serialize(document);
171
+ // proxyRes.headers['content-length'] = output.length
172
+ delete proxyRes.headers['content-length'];
173
+ delete proxyRes.headers['last-modified'];
174
+ delete proxyRes.headers['max-age'];
175
+ delete proxyRes.headers.etag;
176
+ proxyRes.headers['cache-control'] = 'no-cache';
177
+ }
178
+ res.writeHead(proxyRes.statusCode ?? 200, proxyRes.headers);
179
+ res.end(output);
180
+ }
181
+ catch (err) {
182
+ console.error(err);
183
+ res.writeHead(500);
184
+ res.end(err.message);
185
+ }
186
+ });
187
+ }
188
+ }
189
+ }));
190
+ for (const proxyPath of config.app.proxyPaths) {
191
+ app.use(proxyPath, createProxyMiddleware({
192
+ target: appUrl.origin,
193
+ secure: false,
194
+ changeOrigin: true,
195
+ ws: true
196
+ }));
197
+ }
198
+ // re-expose a data-fair instance to access datasets, etc.
199
+ const dfUrl = new URL(config.dataFair.url);
200
+ app.use('/data-fair', createProxyMiddleware({
201
+ target: dfUrl.origin,
202
+ secure: false,
203
+ changeOrigin: true,
204
+ selfHandleResponse: true, // so that the onProxyRes takes care of sending the response
205
+ on: {
206
+ proxyReq(proxyReq, req, res) {
207
+ proxyReq.path = '/data-fair' + proxyReq.path;
208
+ // no gzip so that we can process the content
209
+ proxyReq.setHeader('accept-encoding', 'identity');
210
+ // authentication through api key only
211
+ proxyReq.setHeader('cookie', '');
212
+ if (config.dataFair.apiKey)
213
+ proxyReq.setHeader('x-apiKey', config.dataFair.apiKey);
214
+ // body was already parsed by body-parser and no longer available as a stream
215
+ fixRequestBody(proxyReq, req);
216
+ },
217
+ proxyRes(proxyRes, req, res) {
218
+ // console.log('DF RES', proxyRes)
219
+ if (proxyRes.headers['content-type'] && proxyRes.headers['content-type'].startsWith('application/json')) {
220
+ let body = '';
221
+ proxyRes.on('data', (data) => { body += data.toString(); });
222
+ proxyRes.on('end', () => {
223
+ const output = body.replace(new RegExp(escapeStringRegexp(config.dataFair.url), 'g'), 'http://localhost:5888/data-fair');
224
+ // proxyRes.headers['content-length'] = output.length
225
+ delete proxyRes.headers['content-length'];
226
+ res.writeHead(proxyRes.statusCode ?? 200, proxyRes.headers);
227
+ // make all references to data-fair url point to local proxy
228
+ res.end(output);
229
+ });
230
+ }
231
+ else {
232
+ res.writeHead(proxyRes.statusCode ?? 200, proxyRes.headers);
233
+ proxyRes.pipe(res);
234
+ }
235
+ }
236
+ }
237
+ }));
238
+ // also re-expose the simple-directory instance matching data-fair
239
+ app.use('/simple-directory', createProxyMiddleware({
240
+ target: dfUrl.origin,
241
+ secure: false,
242
+ changeOrigin: true,
243
+ on: {
244
+ proxyReq(proxyReq, req, res) {
245
+ proxyReq.path = '/simple-directory' + proxyReq.path;
246
+ }
247
+ }
248
+ }));
249
+ if (config.serveUi) {
250
+ app.use(await createSpaMiddleware(resolve(import.meta.dirname, '../ui/dist'), uiConfig));
251
+ }
252
+ else {
253
+ app.use('/', createProxyMiddleware({
254
+ target: 'http://localhost:6220',
255
+ secure: false
256
+ }));
257
+ }
258
+ // Run app and return it in a promise
259
+ export const run = async () => {
260
+ server.listen(config.port);
261
+ await eventPromise(server, 'listening');
262
+ console.log(chalk.bold.blue('\nDataFair dev server available on ') + chalk.underline.bold.blue(`http://localhost:${config.port}`));
263
+ return server;
264
+ };
265
+ export const stop = async () => {
266
+ if (devServerWS)
267
+ devServerWS.terminate();
268
+ if (dataFairWS)
269
+ dataFairWS.terminate();
270
+ server.close();
271
+ await eventPromise(server, 'close');
272
+ await app.get('client').close();
273
+ };